Link to this headingPBKDF1

  • Do not Use
  • Using HMAC Key Derivation Functions (HKDF) is less secure than using [PBKDF2](/Crypto/Key Derivation/PBKDF2), [Bcrypt](/Crypto/Hash Functions/BCrypt), [Scrypt](/Crypto/Key Derivation/Scrypt) or [Argon2](/Crypto/Key Derivation/Argon2).

Since the Input to the function is of fixed size and exactly equal to the output size there is a smaller amount of possible inputs and also it makes the 2 iteration have the same possible inputs as the 10th iteration.

Either there will be a larger possibility for collision of two arbitrary messages.
Or there will be a cycle where hashing a message creates the same output as hashing the same message N times.

Link to this headingSecurity

Link to this headingCycle Detection Attacks

Source

Because the default version of this just takes the output hash and uses it as the full message for the next cycle there is a reduction in the randomness and can possobly converge. This is improbable for strong hash algoruthums but is possoble for weaker ones.

PoC for a non-secure Hashfunction:

import zlib, struct, hashlib from typing import Tuple, Optional def crc16_trunc_bytes(data: bytes) -> bytes: """Truncated CRC32 -> 16-bit big-endian two-byte digest.""" v = zlib.crc32(data) & 0xFFFFFFFF return struct.pack(">H", v & 0xFFFF) def pbkdf1_like(hash_fn, password: bytes, salt: bytes, iterations: int, dklen: int = None) -> bytes: t = hash_fn(password + salt) for i in range(1, iterations): t = hash_fn(t) return t if dklen is None else t[:dklen] def floyd_cycle_detection_bytes(f, x0: bytes, max_steps: int = 5000) -> Tuple[bool, Optional[int], Optional[int], Optional[bytes], int]: tortoise = f(x0) hare = f(f(x0)) steps = 0 while tortoise != hare and steps < max_steps: tortoise = f(tortoise) hare = f(f(hare)) steps += 1 if tortoise != hare: return False, None, None, None, steps # find mu mu = 0 tortoise = x0 while tortoise != hare: tortoise = f(tortoise) hare = f(hare) mu += 1 # find lambda lam = 1 hare = f(tortoise) while tortoise != hare: hare = f(hare) lam += 1 return True, mu, lam, tortoise, steps # Use the concrete bytes found earlier combined = b'&\x8d=\x03\xc1' # bytes: 26 8d 3d 03 c1 password = combined[:2] # b'&\x8d' salt = combined[2:] # b'=\x03\xc1' print("Using combined bytes (password||salt):", combined.hex(), " -> password:", password.hex(), " salt:", salt.hex()) # Compute T0 t0 = crc16_trunc_bytes(password + salt) print("T0 = H(password||salt) =", t0.hex()) # Show a short sequence of iterates print("\nSequence of iterates (first 8):") x = t0 for i in range(8): print(f" {i:2d}: {x.hex()}") x = crc16_trunc_bytes(x) # Floyd cycle detection starting at T0 found, mu, lam, meet_state, steps = floyd_cycle_detection_bytes(crc16_trunc_bytes, t0, max_steps=2000) print("\nFloyd cycle detection result:") print(" found:", found, "mu:", mu, "lambda:", lam, "meeting_state:", (meet_state.hex() if meet_state else None), "steps_taken:", steps) # Show derived digest for several iteration counts for iters in [1,2,3,10,100,1000]: dk = pbkdf1_like(crc16_trunc_bytes, password, salt, iterations=iters, dklen=None) print(f"Derived digest after {iters} iterations: {dk.hex()}") # Also demonstrate that applying H to the 2-byte state yields same value (fixed point) t1 = crc16_trunc_bytes(t0) print("\nT1 = H(T0) =", t1.hex()) print("T0 == T1 ?", t0 == t1) """ Using combined bytes (password||salt): 268d3d03c1 -> password: 268d salt: 3d03c1 T0 = H(password||salt) = 9479 Sequence of iterates (first 8): 0: 9479 1: 9479 2: 9479 3: 9479 4: 9479 5: 9479 6: 9479 7: 9479 Floyd cycle detection result: found: True mu: 0 lambda: 1 meeting_state: 9479 steps_taken: 0 Derived digest after 1 iterations: 9479 Derived digest after 2 iterations: 9479 Derived digest after 3 iterations: 9479 """

Link to this headingImplementation

How it works:

import hashlib def fixedlen_xor(input1, input2): assert(len(input1) == len(input2)) return bytes([input1[i] ^ input2[i] for i in range(len(input1))]) def int_to_bytes_length(i_data, length, be=True): if be: return (i_data).to_bytes(length, byteorder='big') else: return (i_data).to_bytes(length, byteorder='little') def hmac(key, message, hash_function): #Get hash_function block_size block_size = getattr(hash_function(), 'block_size') # Check if key is longer than block size. if len(key) > block_size: # IF it is then hash the key. This makes the keysize the same as the output of the hashfunction key = hash_function(key).digest() # IF key is shorter if len(key) < block_size: # Pad the key to blocksize key = key + b"\x00" * (block_size - len(key)) #print(key, len(key), block_size) # Create Keys o_key = fixedlen_xor(key, b"\x5c" * block_size) i_key = fixedlen_xor(key, b"\x36" * block_size) #Hash i_key and message tmp = hash_function(i_key + message) #Hash the o_key and the hashed output of above return hash_function(o_key + tmp.digest()).digest() def pbkdf1(password, salt, iterations=1000, keylength=24, hashobj=hashlib.sha1): output_hash = hashobj(password + salt).digest() #Check if keylength is too big for hash function if len(output_hash) < keylength: raise Exception("Invalid length {} for hash function".format(keylength)) #Do Loop for iterations for idx in range(iterations): output_hash = hashobj(output_hash).digest() #Return the hash with the correct size return output_hash[:keylength] if __name__ == '__main__': print(pbkdf1(b'password', b'salt', 1, 20, hashlib.sha1).hex()) #47e97e39e2b32b15eb9278e53f7bfca57f8e6b2c